// ============================================================================
// FileName: ProxyMonitor.cs
//
// Description:
// Logs proxy events so that the proxy can be monitored and events watched/logged.
//
// Author(s):
// Aaron Clauson
//
// History:
// 01 May 2006	Aaron Clauson	Created.
// ============================================================================

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using mimosa.Net;
using mimosa.SIP;
using mimosa.Utilities;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace mimosa.SIPServer
{	
	public delegate bool ProxyAuthenticateDelegate(string username, string password);
    
    public class ProxyMonitorClient
	{
        public DateTime Created = DateTime.Now;
        public ProxyMonitorFilter Filter;

        public Socket ClientSocket = null;         // If non null indicates the proxy client is a telnet socket.

        public string Filename = null;             // If non null indicates the proxy client is a log file.
        public int LogDurationMinutes = 0;
        public Stream FileStream = null;

        public bool Remove = false;                 // Set to true when the proxy client should be deactivated.

        public ProxyMonitorClient(Socket socket, ProxyMonitorFilter filter)
		{
			ClientSocket = socket;
			Filter = filter;
		}

        public ProxyMonitorClient(string filename, ProxyMonitorFilter filter)
        {
            Filename = filename;
            LogDurationMinutes = filter.FileLogDuration;
            Filter = filter;

            FileStream = new FileStream(filename, FileMode.Create);
            string logStartedMessage = "Log started at " + Created.ToString("dd MMM yyyy HH:mm:ss") + " requested duration " + LogDurationMinutes + ".";
            FileStream.Write(Encoding.ASCII.GetBytes(logStartedMessage), 0, logStartedMessage.Length);
            FileStream.Flush();
        }
	}

	public class ProxyMonitor
	{	
		private const int MAX_THREAD_WAITTIME = 1;		// 1 second.
		private const int EVENTLOG_QUEUE_THRESHHOLD = 100;
		private const int SIGNALLOG_QUEUE_THRESHHOLD = 100;
		private const int MAX_USERAGENT_LENGTH = 256;
		private const int MAX_PROXY_CLIENTS = 10;
        private const int MAX_LOGIN_ATTEMPTS = 3;

		private static ILog logger = AppState.logger;

        public event ProxyAuthenticateDelegate ProxyAuthenticate;

		private IPEndPoint[] m_monitorEndPoints;
		public bool ListenForClients = true;
        private int m_eventSinkPort = 0;
        private string m_fileLogDirectory;
        private string m_username = null;
        private string m_superUsername = null;

        private List<ProxyMonitorClient> m_clients = new List<ProxyMonitorClient>();

		//public ProxyMonitorFilter LogFilter = new ProxyMonitorFilter(null);	// Filter used for the main process.

        public ProxyMonitor(IPEndPoint[] monitorEndPoints, int eventSinkPort, string fileLogDirectory, string superUsername)
		{
			m_monitorEndPoints = monitorEndPoints;
            m_eventSinkPort = eventSinkPort;
            m_fileLogDirectory = fileLogDirectory;
            m_superUsername = superUsername;
		}

        public void StartMonitoring()
        {
            Thread monitorThread = new Thread(new ThreadStart(StartProxyMonitoring));
            monitorThread.Start();
        }

        private void StartProxyMonitoring()
		{
			try
			{
				UdpClient serverClient = new UdpClient(m_eventSinkPort, AddressFamily.InterNetwork);
				IPEndPoint remoteEP = null;

                foreach (IPEndPoint monitorEndPoint in m_monitorEndPoints)
                {
                    ThreadPool.QueueUserWorkItem(new WaitCallback(AcceptClients), monitorEndPoint);
                }

				while(true)
				{
					byte[] buffer = serverClient.Receive(ref remoteEP);
					
					if(m_clients.Count > 0)
					{
						//string message = Encoding.ASCII.GetString(buffer, 0, buffer.Length);
                        //logger.Debug(message);
						//ForwardEventMessage(message);
						//Console.WriteLine(Encoding.ASCII.GetString(buffer, 0, buffer.Length));
						
                        if (m_clients.Count > 0)
						{
                            ProxyMonitorEvent proxyEvent = ProxyMonitorEvent.ParseEventCSV(Encoding.ASCII.GetString(buffer, 0, buffer.Length));
                            string messageStr = proxyEvent.ServerType + " " + proxyEvent.EventType + " " + proxyEvent.Created.ToString("dd MMM yyyy HH:mm:ss") + "\r\n" + proxyEvent.Message + "\r\n";
                            ProcessEvent(proxyEvent);
						}
					}
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception ProxyMonitor StartMonitoring. " + excp.Message);
			}
		}
		
		public void AcceptClients(object state)
		{
            try
            {
                IPEndPoint monitorEndPoint = (IPEndPoint)state;
                TcpListener monitorServer = new TcpListener(monitorEndPoint);
                monitorServer.Start();

                logger.Debug("ProxyMonitor listening for clients on " + monitorEndPoint.ToString() + ".");

                while (ListenForClients)
                {
                    Socket clientSocket = monitorServer.AcceptSocket();
                    IPEndPoint clientEndPoint = (IPEndPoint)clientSocket.RemoteEndPoint;

                    logger.Debug("ProxyMonitor new client connection from " + clientEndPoint.ToString());

                    ThreadPool.QueueUserWorkItem(new WaitCallback(StartMonitorClient), clientSocket);
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception ProxyMonitor AcceptClients. " + excp.Message);
            }
		}				
		
		private void ProcessEvent(ProxyMonitorEvent proxyEvent)
		{
            try
            {
                List<ProxyMonitorClient> removals = new List<ProxyMonitorClient>();
                
                lock (m_clients)
                {
                    if (m_clients.Count > 0)
                    {
                        foreach (ProxyMonitorClient client in m_clients)
                        {
                            if (client.Remove)
                            {
                                if (!removals.Contains(client))
                                {
                                    removals.Add(client);
                                }

                                continue;
                            }
                            
                            //logger.Debug("event=" + proxyEvent.EventType.ToString() + ", username= " + proxyEvent.Username);

                            ProxyMonitorFilter filter = client.Filter;
                            if (filter != null && filter.ShowProxyMonitorEvent(proxyEvent))
                            {
                                string socketEventMessage = proxyEvent.EventType.ToString() + " " + proxyEvent.Created.ToString("HH:mm:ss:fff");
                                if (proxyEvent.EventType == ProxyEventTypesEnum.FullSIPTrace)
                                {
                                    socketEventMessage += ":\r\n" + proxyEvent.Message + "\r\n";
                                }
                                else
                                {
                                    socketEventMessage += ": " + proxyEvent.Message + "\r\n";
                                }

                                byte[] socketEventBytes = Encoding.ASCII.GetBytes(socketEventMessage);

                                if (client.ClientSocket != null)
                                {
                                    client.ClientSocket.Send(socketEventBytes);
                                }
                                else if (client.FileStream != null)
                                {
                                    // Check the duration of the filelog has not been exceeded.
                                    if (DateTime.Now.Subtract(client.Created).TotalMinutes > client.LogDurationMinutes)
                                    {
                                        logger.Debug("Closing file log " + client.Filename + ".");

                                        if (client.FileStream != null)
                                        {
                                            client.FileStream.Close();
                                        }

                                        client.Remove = true;
                                    }
                                    else
                                    {
                                        client.FileStream.Write(socketEventBytes, 0, socketEventBytes.Length);
                                        client.FileStream.Flush();
                                    }
                                }
                                else
                                {
                                    client.Remove = true;
                                }
                            }
                        }
                    }
                }

                if (removals.Count > 0)
                {
                    lock (m_clients)
                    {
                        foreach (ProxyMonitorClient removal in removals)
                        {
                            string clientIdentifier = (removal.ClientSocket != null) ? removal.ClientSocket.ToString() : removal.Filename;
                            logger.Debug("Removing monitor client " + clientIdentifier + ".");
                            m_clients.Remove(removal);
                        }
                    }
                }
            }
            catch (Exception excp)
            {
                logger.Error("Exception ProxyMonitor ProcessEvents. " + excp.Message);
            }
		}
		
		private void StartMonitorClient(object state)
		{
            Socket clientSocket = null;
			IPEndPoint clientEndPoint = null;
            ProxyMonitorClient proxyClient = null;
            ProxyMonitorFilter monitorFilter = null;
            string superUsernameStr = (m_superUsername != null) ? m_superUsername.ToUpper() : null;
			
			try
			{
                clientSocket = (Socket)state;
				clientEndPoint = (IPEndPoint)clientSocket.RemoteEndPoint;

				if(m_clients.Count >= MAX_PROXY_CLIENTS)
				{
					logger.Debug("ProxyMonitor client connection dropped from " + IPSocket.GetSocketString(clientEndPoint) + " as maximum connections exceeded.");
					clientSocket.Send(Encoding.ASCII.GetBytes("maximum connection limit exceeded."));
					clientSocket.Close();
				}
				else
				{
                    if (ProxyAuthenticate != null)
                    {
                        if (!AuthenticateClient(clientSocket))
                        {
                            return;
                        }
                    }				

					byte[] buffer = new byte[1024];
					int bytesRead = 1;

                    monitorFilter = GetClientFilter(clientSocket, m_username.ToUpper() == superUsernameStr);
                    if (monitorFilter == null)
                    {
                        // No filter so close connection
                        return;
                    }

                    clientSocket.Send(Encoding.ASCII.GetBytes("filter=" + monitorFilter.GetFilterDescription() + "\r\n"));

                    proxyClient = new ProxyMonitorClient(clientSocket, monitorFilter);
					m_clients.Add(proxyClient);

					while(bytesRead > 0)
					{
						bytesRead = clientSocket.Receive(buffer, 0, 1024, SocketFlags.None);
						string option = Encoding.ASCII.GetString(buffer, 0, bytesRead);

						if(option == "q") // for quit.
						{
							break;
						}
                        else if (option == "s" && m_username == m_superUsername)
                        {
                            proxyClient.Filter = null;
                            monitorFilter = GetClientFilter(clientSocket, m_username.ToUpper() == superUsernameStr);

                            if (monitorFilter == null)
                            {
                                // Could not get a filter so close connection
                                break;
                            }

                            clientSocket.Send(Encoding.ASCII.GetBytes("filter=" + monitorFilter.GetFilterDescription() + "\r\n"));

                            proxyClient.Filter = monitorFilter;
                        }
					}	
				}
			}
			catch(Exception excp)
			{
				logger.Error("Exception ProxyMonitor StartMonitorClient. " + excp.Message); 
			}
			finally
            {
                #region Removing any expired clients.

                try
				{
                    if (proxyClient != null && m_clients.Contains(proxyClient))
                    {
                        lock (m_clients)
                        {
                            m_clients.Remove(proxyClient);
                        }
                    }

					if(clientSocket != null)
					{
						clientSocket.Close();
					}
				}
				catch{ }

                #endregion
            }
		}

        private ProxyMonitorFilter GetClientFilter(Socket clientSocket, bool isSuperUser)
        {
            ProxyMonitorFilter monitorFilter = null;
            string filter = null;

            byte[] buffer = new byte[1024];
            int bytesRead = 1;

            clientSocket.Send(Encoding.ASCII.GetBytes("\r\nfilter=> "));
         
            while (monitorFilter == null)
            {
                while (bytesRead > 0)
                {
                    bytesRead = clientSocket.Receive(buffer, 0, 1024, SocketFlags.None);
                    filter += Encoding.ASCII.GetString(buffer, 0, bytesRead);

                    if (filter == "q\r\n")
                    {
                        return null;
                    }
                    else if (filter != null && Regex.Match(filter, @"\r\n").Success)
                    {
                        break;
                    }
                }

                try
                {
                    filter = filter.Trim();
                    monitorFilter = new ProxyMonitorFilter(filter);

                    if (!isSuperUser)
                    {
                        // if not a super user the only filters that can be manually set are request and event.
                        logger.Debug("Setting filter username for non-super user, username=" + m_username + ".");
                        ProxyMonitorFilter userFilter = new ProxyMonitorFilter("user " + m_username);
                        userFilter.EventFilterDescr = monitorFilter.EventFilterDescr;
                        userFilter.SIPRequestFilter = monitorFilter.SIPRequestFilter;

                        monitorFilter = userFilter;
                    }
                    else
                    {
                        // If a file log has been specified it is created as a separate proxy client.
                        if (monitorFilter.FileLogname != null && monitorFilter.FileLogname.Trim().Length > 0)
                        {
                            logger.Debug("Monitor client log filename=" + monitorFilter.FileLogname + ".");

                            if (!Regex.Match(monitorFilter.FileLogname, @"\/|\\|\:").Success)
                            {
                                string fileName = m_fileLogDirectory + monitorFilter.FileLogname;
                                logger.Error("Monitor client log filename=" + fileName + ", duration=" + monitorFilter.FileLogDuration + ".");
                                m_clients.Add(new ProxyMonitorClient(fileName, monitorFilter));
                            }
                            else
                            {
                                logger.Error("Illegal monitor client log filename=" + monitorFilter.FileLogname + ".");
                                clientSocket.Send(Encoding.ASCII.GetBytes("Illegal file name."));
                                clientSocket.Close();
                            }
                        }
                    }

                    return monitorFilter;
                }
                catch
                {
                    clientSocket.Send(Encoding.ASCII.GetBytes("filter was invalid, please try again, or q quit.\r\nfilter=> "));
                    filter = null;
                }
            }

            return null;
        }

        private bool AuthenticateClient(Socket clientSocket)
        {
            byte[] buffer = new byte[1024];
            int bytesRead = 1;
            int attempts = 0;

            while (attempts < MAX_LOGIN_ATTEMPTS)
            {
                string username = null;
                string password = null;

                clientSocket.Send(Encoding.ASCII.GetBytes("username=> "));
                while (bytesRead > 0)
                {
                    bytesRead = clientSocket.Receive(buffer, 0, 1024, SocketFlags.None);
                    username += Encoding.ASCII.GetString(buffer, 0, bytesRead);

                    if (username != null && Regex.Match(username, @"\r\n").Success)
                    {
                        username = Regex.Replace(username, @"\s*$", "");
                        break;
                    }
                }

                clientSocket.Send(Encoding.ASCII.GetBytes("password=> "));
                while (bytesRead > 0)
                {
                    bytesRead = clientSocket.Receive(buffer, 0, 1024, SocketFlags.None);
                    password += Encoding.ASCII.GetString(buffer, 0, bytesRead);

                    if (password != null && Regex.Match(password, @"\r\n").Success)
                    {
                        password = Regex.Replace(password, @"\s*$", "");
                        break;
                    }
                }

                if (ProxyAuthenticate(username, password))
                {
                    clientSocket.Send(Encoding.ASCII.GetBytes("welcome " + username + "\r\n"));
                    m_username = username;
                    return true;
                }
                else
                {
                    clientSocket.Send(Encoding.ASCII.GetBytes("login failed.\r\n"));
                    attempts++;
                }
            }

            return false;
        }
	}
}
